教你如何一套源码编译多个APP,不同的签名,包名,界面,字段...
每日推荐
https://github.com/NoEndToLF/Gif-Load-ReTry-Refresh
支持gif图片的Load反馈框架,只需一张gif图,一行代码解决初次加载,重试加载,刷新加载,与生命周期绑定,LeakCanary检测无内存泄漏
本文作者
作者:隔壁王较瘦
链接:
https://www.jianshu.com/p/4a5625a00f8d
本文由作者授权发布。
线上源码地址,结合源码看比较容易理解
源码地址:
https://github.com/lmyDevs/multiApp
作为一个刚入行一两年的 代码 Copy 者 来说,这么久没有深入了解 Gradle 真的是一大遗憾,熟悉了 Gradle 之后开发效率能够提高很多,当然,现在我还没有到那个地步,这篇文章就是我实际开发中的需求,刚开始的时候用的是最基本的开发方式,后来东西越来越多,渐渐的感觉这种开发方式太拖慢进度了,然后就搜索学习一番,没有理解,只是学习了基本的操作来解决开发中的需求,特写篇文章记录一下,怕忘记。
emmm, 我可能有一个这样的需求,不知道可不可以...
我的需求很简单...
小张是一个四肢健全,各项功能正常的人,我需要根据这个人克隆出更多人
我的目的很明确...
我要跟别人长得不一样(界面不一样)
我要身体会特殊的技能(额外功能)
我要改名身份证上的名字(APP 名字)
我要换身份证上的头像 (APP 桌面图标)
我要换身份证号码 (APP 包名)
我要换肤色 (不同的界面颜色,icon等)
我要不同的签证 (不同的应用签名)
ennn, 我可能有这样一种解决办法, 应该可以...
小张的基本信息是这样
信息描述 | 基本信息 |
身份证头像 (APP 名字) | 小张 |
身份证头像 (APP 头像) | |
身份证号码 (APP 包名) | com.persons.zhang |
小张是长这样的
这是小张的源码
class MainActivity : AppCompatActivity() {
private val mOnNavigationItemSelectedListener = BottomNavigationView.OnNavigationItemSelectedListener { item ->
when (item.itemId) {
R.id.navigation_home -> {
viewPager.currentItem = 0
return@OnNavigationItemSelectedListener true
}
R.id.navigation_dashboard -> {
viewPager.currentItem = 1
return@OnNavigationItemSelectedListener true
}
R.id.navigation_notifications -> {
viewPager.currentItem = 2
return@OnNavigationItemSelectedListener true
}
}
false
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
navigation.setOnNavigationItemSelectedListener(mOnNavigationItemSelectedListener)
var fragments : MutableList<Fragment> = mutableListOf()
fragments.add(HeadFragment())
fragments.add(HandFragment())
fragments.add(FootFragment())
viewPager.adapter = MainViewPagerAdapter(supportFragmentManager,fragments)
viewPager.addOnPageChangeListener(object : ViewPager.OnPageChangeListener{
override fun onPageScrollStateChanged(state: Int) {
}
override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {
}
override fun onPageSelected(position: Int) {
navigation.menu.getItem(position)?.setChecked(true)
}
})
}
}
这是小张的布局
<android.support.constraint.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.persons.zhang.MainActivity">
<android.support.v4.view.ViewPager
android:id="@+id/viewPager"
app:layout_constraintBottom_toTopOf="@id/navigation"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<android.support.design.widget.BottomNavigationView
android:id="@+id/navigation"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="0dp"
android:layout_marginStart="0dp"
android:background="?android:attr/windowBackground"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:menu="@menu/navigation" />
</android.support.constraint.ConstraintLayout>
这是我们要增加的小李的大概需求
名字要不一样 (APP 名字)
身份证号码要不一样 (APP 包名)
身份证头像要不一样 (APP 图标)
肤色不一样 (界面颜色)
头跟别人不一样 (主界面的头)
生产之前的准备工作
我们把 Android 切换到 Project
1、新建关于包括小李一切的目录
看着上图,对比着看啊,上图中的 zhang -> app -> src 目录上 右键 -> New -> Directory 新建一个小李的目录,我这里就叫做小李了,下面是图操作:
然后,我们对比着 src/main 这个目录里面的结构,在 li 这个目录下创建一模一样的结构,如下图:
这是li 目录下新建的
这是main 的结构
好了,新建完成。
2. 新建针对小李的Gradle构建变体
我也理解不清楚,大概就是把小李这个人当成备胎,这样这个人什么时候想换备胎了,小李这个人可以是其中一个。就是在编译的时候多一个小李的选项,可以单独针对小李进行编译,也就是备胎转正。
我们在 app的gradle下 添加如下代码
android {
compileSdkVersion 26
buildToolsVersion "26.0.2"
defaultConfig {
applicationId "com.persons.zhang"
minSdkVersion 15
targetSdkVersion 26
versionCode 1
versionName "1.0"
}
...
// 重点在这下面这些
flavorDimensions "app"
// 重点--------------------------
productFlavors {
li {
dimension "app"
}
zhang {
dimension "app"
}
}
}
注意看 productFlavors 里面的 li 和 zhang ,他们里面都有 applicationId、versionCode、versionName 这些,这些就是针对他们不同的人,有不同的版本,不同的包名。
这样写的话必须保证我们新建的那个目录要跟 productFlavors 里面的这个 li { ... } 保持一致 ,如果想使用不一样的名字可以按照以下操作来,至于上面的flavorDimensions 我也不太理解,没使用到。
android {
compileSdkVersion 26
buildToolsVersion "26.0.2"
...
sourceSets {
li {
java.srcDir('src/li/java')
res.srcDir('src/li/res')
}
}
}
这样显示指定源码路径,名字不一致也无所谓了。
这样我们就新建出来了一个小李 ,但是新建出来了我们怎么才能把默认点击 Run 安装的 debug 版本的app改成 小李呢,如果上面已经一步步配置好了之后,会有下图这些东西:
选择完成之后再点击 Run 安装的就是小李的 debug版本了
此时的小李还是和小张一模一样的,下面我们要把小李 改成他该有的模样。
3.不一样的名字
既然我们这个项目都叫做小李了,那我们不能名字还是小张的名字吧,下面我们来给小李改改名字,一般情况下我们的名字都是在 strings.xml 里面的 app_name 字段来定义的,当然不排除你直接在 Manifest 里面写死,那你很棒棒哦
我们的 strings.xml 现在只有 src/main/res/values 里面才有,我们要做的是在 src/li/res/values 里面新建一个 strings.xml ,里面新建一个 app_name 的字段,值为 小李 ,如下图:
小张(也就是main)
小李(也就是li)
不知道你注意到没有,小张(main) 的 strings.xml 里面还有 head 等字段, 小李(li) 却没有。
原因 :资源文件会自动合并。就这么简单,就比如说,现在 张 里面有 head 这些,李 没有,等后面我们修改过,资源文件合并之后会变成以下状态:
相同的被替换,不同的被增加 ,相同的会被替换成当前目标的,比如说你现在小张有app_name,小李
也有,如果现在编译目标 是小李的话,app_name 的值就会是小李,好了我们来运行看一下效果:
好了,看来我们修改成功了,上面小张这个APP是我改了些东西保留下来的,为了作对比,正常情况下以前装的小张会被小李覆盖掉, 下面我们来看一下小李的主界面长什么样子。
可以看到我们的app_name 已经修改成功了,其他没修改的还是按照小张的样子来展示的,包括下面的头,手,脚,这些。
4.身份证号码不一样(包名不一样)
上面我说了,小张被保留下来是我改了一些东西才能保留下来和小李作对比的,正常情况下安装了小李之后,小张就会被覆盖掉,因为他们的包名一样的。
下面我们来更改包名让 App 共存,我们的操作还是在 app/gradle 里面的 productFlavors 进行更改。
android {
compileSdkVersion 26
buildToolsVersion "26.0.2"
...
productFlavors {
li {
applicationId "com.persons.li"
versionCode 2
dimension "app"
versionName "1.0.2"
}
zhang {
applicationId "com.persons.zhang"
versionCode 1
dimension "app"
versionName "1.0"
}
}
}
仔细看,我每个都加了 applicationId ,顺便还加了 versionCode,versionName, 这可以单独设置每个app不同的版本号,不同的版本名称,不同的包名,怕晕了,给个截图让你瞅瞅
5.身份证头像不一样 (APP 图标)
我们把版本号修改了,现在我们安装了之后就可以两个APP共存了,而且包名也是不相同的,这样基本上实现两个人就可以出现在一个手机里面了。
但是只是名字改了,图标还是一样的,猛一看,很懵逼啊,这一不小心就点错了,多尴尬
众所周知,我们的app图标是放在 res/mipmap 目录下的, 我们之前拷贝的 li 目录里面是和 小张(main) 的是一样的,我们在准备工作的时候还设置了 sourceSet ,而且,前面还说了相同名字的资源被覆盖,不同的被增加 ,我们要做的只是把 小李(li) 的图标放在 li/src/res/mipmap 下就可以了, 然后运行安装到手机:
下面看改过的效果
好了,替换完成了。
6.不同的肤色
我们现在小李的肤色还跟小张的肤色是一样的,下面我们来改一下,一切关于资源的要么被替换,要么被合并 。
我们要改的是 li/res/values/colors.xml , 因为现在小李这个目录下面还没有 colors.xml 这个文件,我们直接从 小张(main) 的目录里面 copy 过来一个,改一下里面的色值就可以了。
小张的肤色
小李的肤色
小李改过肤色的样子
好了,肤色也改完了。
7.头界面不一样(重点)
现在小李的头界面还是上面这张图的界面,什么也没有。我们要做的是在头这个界面里面加上眼睛,鼻子,小张是个无面男 ,我们要小李有个鼻子,点击鼻子的时候弹出一个提示。
注意: 这个方法是通用的,就是说,只要是关于 java 代码的都按照这样的套路来做,把要修改的那个类拷贝出来,把基础包里面的删掉,给其他的目录里面也拷贝进去这个类,要不你在编译其他目录的apk的时候会报错的,找不到类。
1.首先我们先把小李的头copy过来。
为什么要拷贝过来? 不拷贝过来怎么会有自己的头,如果没有拷贝过来,默认使用的是小张的头。
注意 :这时我们需要把源码大改一下了,我们要把原本小张的源码给改成通用的,就是把原来小张的源码当成一个基础包,我们要是加人就需要另外新建一个目录来做,就跟我们增加小李一样的。
下面是我们改过的目录结构
可以看到,我们单独把 小张 给新建了一个目录,他的存在方式跟 小李 的是一样的。
为了以防万一,我们还要在 app/gradle 里面修改一下 sourceSet 保证我们的APP源码指向是正确的。
sourceSets {
li {
java.srcDir('src/li/java')
res.srcDir('src/li/res')
// assets.srcDir('src/li/assets')
// manifest.srcFile('src/li/AndroidManifest.xml')
}
zhang {
java.srcDir('src/zhang/java')
res.srcDir('src/zhang/res')
// assets.srcDir('src/zhang/assets')
// manifest.srcFile('src/zhang/AndroidManifest.xml')
}
}
怕看懵逼,加一张图
我们想要小李的头跟其他的不一样的话,需要单独把 copy 出来进行修改,这个跟res 下的文件不一样, res 下的文件相同了会覆盖合并,java 目录下面是会报错的,
因为基础包目录下已经有了相同名字的类了。你在 li 的里面加了相同的 类, 李使用的源码是用 基础包里的来做基础的, 现在这个状态就相当于 你在同一个包里面,创建了两个相同名字的类
2. 现在我们给小李的头界面加上鼻子
首先我们先把 基础包 里面 头 的布局文件 copy 到 li 的layout目录下。
看清楚,是 li 目录下面的 layout 文件夹,拷贝到这里以后我们就跟平常写界面一样修改就可以了。
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<Button
android:id="@+id/click"
android:layout_centerInParent="true"
android:text="我是鼻子"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</RelativeLayout>
class HeadFragment : Fragment() {
override fun onCreateView(inflater: LayoutInflater?, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = inflater?.inflate(R.layout.fragment_head, container, false)
return view
}
override fun onViewCreated(view: View?, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
click.setOnClickListener {
Toast.makeText(activity,"我的鼻子被点了一下",Toast.LENGTH_LONG).show()
}
}
}
就是界面上有一个叫鼻子的按钮,设置点击之后弹出吐司,是不是还很骚气,而我们加上这个鼻子之后,小张还是原来的小张 ,鼻子只是被加在了小李的头上。
这样好几个app在一块编译,我们在androidstudio里面点击打包还要选择不同的 签名文件,很麻烦,我们还是在 app/src/gradle 里面进行配置就可以了。
android {
....
signingConfigs {
// 只是用来做演示,没有特地生成签名文件
li {
storeFile file("src/li/liKeystore.jks")
storePassword "li123"
keyAlias "li"
keyPassword "li1234"
// 开启 V2 签名
v2SigningEnabled true
}
zhang {
storeFile file("src/zhang/liKeystore.jks")
storePassword "zhang123"
keyAlias "zhang"
keyPassword "zhang1234"
v2SigningEnabled true
}
}
productFlavors {
li {
applicationId "com.persons.li"
versionCode 2
dimension "app"
versionName "1.0.2"
// 注意看这里---------------------------------------------
signingConfig signingConfigs.li
}
zhang {
applicationId "com.persons.zhang"
versionCode 1
dimension "app"
versionName "1.0"
signingConfig signingConfigs.zhang
}
}
}
1.批量打包和运行签名包测试第三方登录或分享,或支付。
根据我们上面 gradle 的配置,签名打包很方便,打包使用这种方式,上面可以单独打一个,也可以一次打完,打一个就选择第四步其他的名字,比如 assembleZhang 这些。
打包使用这种方式,上面可以单独打一个,也可以一次打完,打一个就选择第四步其他的名字,比如 assembleZhang 这些。
2. Manifest value 值设置占位符
在 Manifest 中
在 Gradle 中
3. 关于微信回调
微信的回调是需要放在 包名/wxapi/ 目录下的,我们这样拷贝之后没有了包名都是不一样的,我们要为每个app的 src/java 目录下新建一个和包名一样的目录,比如小张, 我们为了方便区分,把小张的包名改成 cn.zhang ,下面是目录结构
到这里基本结束了。
推荐阅读:
Android截屏相关的都在这儿
如果你想要跟大家分享你的文章,欢迎投稿~